/*
 * PropertyChangeSupport.java  0.4.0 / Oct 24, 2010
 *
 * Copyright (c) 2010 - 2011, cute4j.com and contributors.
 *
 * This software is licensed under the LGPL version 2.1 license.
 * A copy of this license is included in the file LICENSE-LGPL.txt.
 */
package com.cute4j.swing.events;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;

/**
 * Property Change Support.
 *
 * @author Slobodan Vrkacevic
 * @version 0.4.0
 */
public class PropertyChangeSupport implements Cloneable {

// Private Static Fields. ------------------------------------------------------

  private static final PropertyChangeListener[]
      EMPTY_PROPERTY_CHANGE_LISTENER_ARRAY = new PropertyChangeListener[0];

// Private Fields. -------------------------------------------------------------

  private final Object source;
  private PropertyChangeListener[] listeners;
  private int listenerCount;
  private HashMap listenerGroups;
  private HashMap listenerGroupCounts;

// Public Constructors. --------------------------------------------------------

  /**
   * Constructs new PropertyChangeSupport object.
   *
   * @param source the source object
   *
   * @throws IllegalArgumentException if the source if <tt>null</tt>
   */
  public PropertyChangeSupport(Object source) {
    if (source == null) {
      throw new IllegalArgumentException("Null source");
    }
    this.source = source;
    listeners = new PropertyChangeListener[3];
    listenerGroups = new HashMap(3);
    listenerGroupCounts = new HashMap(3);
  }

// Public Static Methods. ------------------------------------------------------

  /**
   * Returns empty array of PropertyChangeListeners.
   *
   * @return empty array of PropertyChangeListeners
   */
  public static PropertyChangeListener[] getEmptyPropertyChangeListenerArray() {
    return EMPTY_PROPERTY_CHANGE_LISTENER_ARRAY; // NOPMD NOFB
  }

// Public Methods. -------------------------------------------------------------

  /**
   * Adds property change listener for all properties.
   *
   * @param listener the property change listener
   *
   * @throws IllegalArgumentException if the listener is <tt>null</tt>
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    if (listener == null) {
      throw new IllegalArgumentException("Null listener");
    }
    int oldCount = listeners.length;
    if (listenerCount == oldCount) {
      PropertyChangeListener[] oldListeners = listeners;
      int lenght = (3 * oldCount) >> 1;
      listeners = new PropertyChangeListener[++lenght];
      System.arraycopy(oldListeners, 0, listeners, 0, oldCount);
    }
    listeners[listenerCount++] = listener;
  }

  /**
   * Adds property change listener for specified property.
   *
   * @param propertyName the property name
   * @param listener the property change listener
   *
   * @throws IllegalArgumentException if the propertyName is <tt>null</tt>
   * @throws IllegalArgumentException if the listener is <tt>null</tt>
   */
  public void addPropertyChangeListener(String propertyName,
                                        PropertyChangeListener listener) {
    if (propertyName == null) {
      throw new IllegalArgumentException("Null proprty name");
    }
    if (listener == null) {
      throw new IllegalArgumentException("Null listener");
    }
    int size = 0;
    PropertyChangeListener[] listeners;
    Integer count = (Integer)listenerGroupCounts.get(propertyName);
    if (count == null) {
      listeners = new PropertyChangeListener[3];
      listenerGroups.put(propertyName, listeners);
    } else {
      size = count.intValue();
      listeners = (PropertyChangeListener[])listenerGroups.get(propertyName);
      if (size == listeners.length) {
        PropertyChangeListener[] oldListeners = listeners;
        int lenght = (3 * size) >> 1;
        listeners = new PropertyChangeListener[++lenght];
        System.arraycopy(oldListeners, 0, listeners, 0, size);
        listenerGroups.put(propertyName, listeners);
      }
    }
    listeners[size] = listener;
    listenerGroupCounts.put(propertyName, new Integer(++size));
  }

  /**
   * Removes property change listener for all properties.
   *
   * @param listener the property change listener
   */
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    if (listener != null) {
      final PropertyChangeListener[] listeners = this.listeners;
      for (int index = listenerCount; --index >= 0;) {
        if (listeners[index] == listener) {
          int numMoved = listenerCount - index - 1;
          int halfCapacity = listeners.length >> 1;
          if (listenerCount <= halfCapacity) {
            this.listeners = new PropertyChangeListener[halfCapacity];
            if (numMoved > 0) {
              System.arraycopy(listeners, 0, this.listeners, 0, index);
              System.arraycopy(listeners, index + 1, this.listeners, index,
                               numMoved);
              listenerCount--;
            } else {
              System.arraycopy(listeners, 0, this.listeners, 0,
                               --listenerCount);
            }
          } else {
            if (numMoved > 0) {
              System.arraycopy(listeners, index + 1, listeners, index,
                               numMoved);
            }
            listeners[--listenerCount] = null;
          }
          break;
        }
      }
    }
  }

  /**
   * Removes property change listener for specified property.
   *
   * @param propertyName the property name
   * @param listener the property change listener
   */
  public void removePropertyChangeListener(String propertyName,
                                           PropertyChangeListener listener) {
    if (listener != null) {
      Integer count = (Integer)listenerGroupCounts.get(propertyName);
      if (count != null) {
        int size = count.intValue();
        PropertyChangeListener[] listeners =
            (PropertyChangeListener[])listenerGroups.get(propertyName);
        int index = -1;
        for (int i = size; --i >= 0;) {
          if (listeners[i] == listener) {
            index = i;
            break;
          }
        }
        if (index < 0) {
          return;
        }
        if ((size == 1) && (index == 0)) {
          listeners[0] = null;
          listenerGroupCounts.remove(propertyName);
          listenerGroups.remove(propertyName);
          return;
        }
        PropertyChangeListener[] oldListeners = listeners;
        int halfLength = listeners.length >> 1;
        boolean split = size < halfLength;
        if (split) {
          listeners = new PropertyChangeListener[halfLength];
          listenerGroups.put(propertyName, listeners);
        }
        int numMoved = size - index - 1;
        if (numMoved > 0) {
          System.arraycopy(oldListeners, index + 1, listeners, index, numMoved);
        }
        size--;
        if (!split) {
          listeners[size] = null;
        }
        listenerGroupCounts.put(propertyName, new Integer(size));
      }
    }
  }

  /**
   * Returns <tt>true</tt> if there are one or more listeners.
   *
   * @return <tt>true</tt> if there are one or more listeners
   */
  public boolean hasListeners() {
    return ((listenerCount > 0) || (listenerGroupCounts.size() > 0));
  }

  /**
   * Returns <tt>true</tt> if there are listeners for specified property.
   *
   * @param propertyName the name of the property
   *
   * @return <tt>true</tt> if there are listeners for specified property
   */
  public boolean hasListeners(String propertyName) {
    return ((propertyName != null) &&
            listenerGroupCounts.containsKey(propertyName));
  }

  /**
   * Returns the number of all properties listeners.
   *
   * @return the number of all properties listeners
   */
  public int getListenerCount() {
    return listenerCount;
  }

  /**
   * Returns property change listeners for all properties.
   *
   * @return property change listeners for all properties
   */
  public PropertyChangeListener[] getPropertyChangeListeners() {
    int lcnt = listenerCount;
    PropertyChangeListener[] listeners = new PropertyChangeListener[lcnt];
    if (lcnt > 0) {
      System.arraycopy(this.listeners, 0, listeners, 0, lcnt);
    }
    return listeners;
  }

  /**
   * Returns listeners for all properties.
   * <p>
   *   <tt>WARNING: This method does not return copy of the listeners!</tt>
   * </p>
   *
   * @return listeners for all properties
   */
  public PropertyChangeListener[] getPropertyChangeListenersArray() {
    return listeners; // NOPMD NOFB
  }

  /**
   * Returns the number of listeners for specified property.
   *
   * @param propertyName the specified property
   *
   * @return the number of listeners for specified property
   */
  public int getListenerCount(String propertyName) {
    Integer listenerCount = (Integer)listenerGroupCounts.get(propertyName);
    if (listenerCount != null) {
      return listenerCount.intValue();
    }
    return 0;
  }

  /**
   * Returns listeners for the specified property.
   * <p>
   *   <tt>WARNING: This method does not return copy of the listeners!</tt>
   * </p>
   *
   * @param propertyName the specified property
   *
   * @return listeners for the specified property
   */
  public PropertyChangeListener[] getPropertyChangeListenersArray(
                                    String propertyName) {
    return (PropertyChangeListener[])listenerGroups.get(propertyName);
  }

  /**
   * Returns property change listeners for all properties.
   *
   * @param propertyName the property name
   *
   * @return property change listeners for all properties
   */
  public PropertyChangeListener[] getPropertyChangeListeners(
                                    String propertyName) {
    int lcnt = getListenerCount(propertyName);
    PropertyChangeListener[] listeners = new PropertyChangeListener[lcnt];
    if (lcnt > 0) {
      System.arraycopy(listenerGroups.get(propertyName), 0, listeners, 0, lcnt);
    }
    return listeners;
  }

  /**
   * Fires object property change.
   *
   * @param e property change event
   */
  public void firePropertyChange(PropertyChangeEvent e) {
    Object oldValue = e.getOldValue();
    Object newValue = e.getNewValue();
    if (oldValue == null) {
      if (newValue == null) {
        return;
      }
    } else {
      if (oldValue.equals(newValue)) {
        return;
      }
    }
    PropertyChangeListener[] listeners = this.listeners;
    for (int i = listenerCount; --i >= 0;) {
      listeners[i].propertyChange(e);
    }
    String propertyName = e.getPropertyName();
    Integer listenerCount = (Integer)listenerGroupCounts.get(propertyName);
    if (listenerCount != null) {
      listeners = (PropertyChangeListener[])listenerGroups.get(propertyName);
      for (int i = listenerCount.intValue(); --i >= 0;) {
        listeners[i].propertyChange(e);
      }
    }
  }

  /**
   * Fires object property change.
   *
   * @param propertyName property name
   * @param oldValue old value
   * @param newValue new value
   */
  public void firePropertyChange(String propertyName, Object oldValue,
                                 Object newValue) {
    if ((oldValue == null) || (newValue == null) ||
        !oldValue.equals(newValue)) {
      firePropertyChange(new PropertyChangeEvent(source, propertyName, oldValue,
                                                 newValue));
    }
  }

  /**
   * Fires boolean property change.
   *
   * @param propertyName property name
   * @param oldValue old value
   * @param newValue new value
   */
  public void firePropertyChange(String propertyName, boolean oldValue,
                                 boolean newValue) {
    if (oldValue != newValue) {
      firePropertyChange(propertyName, Boolean.valueOf(oldValue),
                         Boolean.valueOf(newValue));
    }
  }

  /**
   * Fires char property change.
   *
   * @param propertyName property name
   * @param oldValue old value
   * @param newValue new value
   */
  public void firePropertyChange(String propertyName, char oldValue,
                                 char newValue) {
    if (oldValue != newValue) {
      firePropertyChange(propertyName, new Character(oldValue),
                         new Character(newValue));
    }
  }

  /**
   * Fires short property change.
   *
   * @param propertyName property name
   * @param oldValue old value
   * @param newValue new value
   */
  public void firePropertyChange(String propertyName, short oldValue,
                                 short newValue) {
    if (oldValue != newValue) {
      firePropertyChange(propertyName, new Short(oldValue),
                         new Short(newValue));
    }
  }

  /**
   * Fires int property change.
   *
   * @param propertyName property name
   * @param oldValue old value
   * @param newValue new value
   */
  public void firePropertyChange(String propertyName, int oldValue,
                                 int newValue) {
    if (oldValue != newValue) {
      firePropertyChange(propertyName, new Integer(oldValue),
                         new Integer(newValue));
    }
  }

  /**
   * Fires float property change.
   *
   * @param propertyName property name
   * @param oldValue old value
   * @param newValue new value
   */
  public void firePropertyChange(String propertyName, float oldValue,
                                 float newValue) {
    if (Float.floatToIntBits(oldValue) != Float.floatToIntBits(newValue)) {
      firePropertyChange(propertyName, new Float(oldValue),
                         new Float(newValue));
    }
  }

  /**
   * Fires double property change.
   *
   * @param propertyName property name
   * @param oldValue old value
   * @param newValue new value
   */
  public void firePropertyChange(String propertyName, double oldValue,
                                 double newValue) {
    if (Double.doubleToLongBits(oldValue) !=
        Double.doubleToLongBits(newValue)) {
      firePropertyChange(propertyName, new Double(oldValue),
                         new Double(newValue));
    }
  }

// Overridden Methods. ---------------------------------------------------------

  /**
   * {@inheritDoc}
   */
  public Object clone() {
    try {
      PropertyChangeSupport clone = (PropertyChangeSupport)super.clone();
      clone.listeners = (PropertyChangeListener[])listeners.clone();
      clone.listenerGroups = (HashMap)listenerGroups.clone();
      clone.listenerGroupCounts = (HashMap)listenerGroupCounts.clone();
      return clone;
    } catch (CloneNotSupportedException e) {
      throw new Error(e); // NOPMD
    }
  }

}
